Kuasai hook useFormState React. Panduan komprehensif untuk manajemen state form yang efisien, validasi sisi server, dan pengalaman pengguna yang lebih baik dengan Server Actions.
React useFormState: Kupas Tuntas Manajemen dan Validasi Form Modern
Formulir adalah landasan interaktivitas web. Dari formulir kontak sederhana hingga wizard multi-langkah yang kompleks, formulir sangat penting untuk input pengguna dan pengiriman data. Selama bertahun-tahun, developer React telah menavigasi lanskap solusi manajemen state, mulai dari hook useState sederhana untuk skenario dasar hingga pustaka pihak ketiga yang kuat seperti Formik dan React Hook Form untuk kebutuhan yang lebih kompleks. Meskipun alat-alat ini sangat baik, React terus berkembang untuk menyediakan primitif yang lebih terintegrasi dan kuat.
Memperkenalkan useFormState, sebuah hook yang diperkenalkan di React 18. Awalnya dirancang untuk bekerja secara mulus dengan React Server Actions, useFormState menawarkan pendekatan yang efisien, kuat, dan asli untuk mengelola state formulir, terutama saat berhadapan dengan logika dan validasi sisi server. Ini menyederhanakan proses menampilkan umpan balik dari server, seperti pesan kesalahan validasi atau pesan keberhasilan, langsung di dalam UI Anda.
Panduan komprehensif ini akan membawa Anda menyelami lebih dalam hook useFormState. Kita akan menjelajahi konsep intinya, implementasi praktis, pola-pola lanjutan, dan bagaimana hook ini cocok dengan ekosistem pengembangan React modern yang lebih luas. Baik Anda membangun aplikasi dengan Next.js, Remix, atau React murni, memahami useFormState akan membekali Anda dengan alat yang ampuh untuk membangun formulir yang lebih baik dan lebih tangguh.
Apa itu `useFormState` dan Mengapa Kita Membutuhkannya?
Pada intinya, useFormState adalah hook yang dirancang untuk memperbarui state berdasarkan hasil dari sebuah aksi formulir. Anggap saja ini sebagai versi khusus dari useReducer yang disesuaikan secara spesifik untuk pengiriman formulir. Hook ini dengan elegan menjembatani kesenjangan antara interaksi pengguna di sisi klien dan pemrosesan di sisi server.
Sebelum useFormState, alur pengiriman formulir yang melibatkan server biasanya terlihat seperti ini:
- Pengguna mengisi formulir.
- State sisi klien (misalnya, menggunakan
useState) melacak nilai input. - Saat pengiriman, sebuah event handler (
onSubmit) mencegah perilaku default browser. - Permintaan
fetchdibuat secara manual dan dikirim ke endpoint API server. - State loading dikelola (misalnya,
const [isLoading, setIsLoading] = useState(false)). - Server memproses permintaan, melakukan validasi, dan berinteraksi dengan database.
- Server mengirimkan kembali respons JSON (misalnya,
{ success: false, errors: { email: 'Invalid format' } }). - Kode di sisi klien mengurai respons ini dan memperbarui variabel state lain untuk menampilkan pesan kesalahan atau keberhasilan.
Proses ini, meskipun fungsional, melibatkan banyak kode boilerplate untuk mengelola state loading, state error, dan siklus permintaan/respons. useFormState, terutama ketika dipasangkan dengan Server Actions, secara dramatis menyederhanakan ini dengan menciptakan alur yang lebih deklaratif dan terintegrasi.
Manfaat utama menggunakan useFormState adalah:
- Integrasi Server yang Mulus: Ini adalah solusi asli untuk menangani respons dari Server Actions, menjadikan validasi sisi server sebagai warga kelas satu di komponen Anda.
- Manajemen State yang Disederhanakan: Ini memusatkan logika untuk pembaruan state formulir, mengurangi kebutuhan akan beberapa hook
useStateuntuk data, error, dan status pengiriman. - Progressive Enhancement: Formulir yang dibangun dengan
useFormStatedan Server Actions dapat berfungsi bahkan jika JavaScript dinonaktifkan di klien, karena dibangun di atas dasar pengiriman formulir HTML standar. - Pengalaman Pengguna yang Ditingkatkan: Memudahkan pemberian umpan balik yang langsung dan kontekstual kepada pengguna, seperti pesan kesalahan validasi inline atau pesan keberhasilan, segera setelah formulir dikirim.
Memahami Tanda Tangan Hook `useFormState`
Untuk menguasai hook ini, mari kita uraikan terlebih dahulu tanda tangan dan nilai kembaliannya. Ini lebih sederhana dari yang terlihat pada awalnya.
const [state, formAction] = useFormState(action, initialState);
Parameter:
action: Ini adalah fungsi yang akan dieksekusi saat formulir dikirim. Fungsi ini menerima dua argumen: state sebelumnya dari formulir dan data formulir yang dikirim. Fungsi ini diharapkan mengembalikan state yang baru. Ini biasanya adalah Server Action, tetapi bisa berupa fungsi apa pun.initialState: Ini adalah nilai yang Anda inginkan untuk state awal formulir, sebelum pengiriman apa pun terjadi. Ini bisa berupa nilai serializable apa pun (string, angka, objek, dll.).
Nilai Kembalian:
useFormState mengembalikan sebuah array dengan tepat dua elemen:
state: State saat ini dari formulir. Pada render awal, ini akan menjadiinitialStateyang Anda berikan. Setelah pengiriman formulir, ini akan menjadi nilai yang dikembalikan oleh fungsiactionAnda. State inilah yang Anda gunakan untuk merender umpan balik UI, seperti pesan kesalahan.formAction: Sebuah fungsi action baru yang Anda teruskan ke propactionelemen<form>Anda. Ketika action ini dipicu (oleh pengiriman formulir), React akan memanggil fungsiactionasli Anda dengan state sebelumnya dan data formulir, lalu memperbaruistatedengan hasilnya.
Pola ini mungkin terasa akrab jika Anda pernah menggunakan useReducer. Fungsi action seperti reducer, initialState adalah state awal, dan React menangani pengiriman untuk Anda saat formulir dikirim.
Contoh Praktis Pertama: Formulir Langganan Sederhana
Mari kita buat formulir langganan buletin sederhana untuk melihat useFormState beraksi. Kita akan memiliki satu input email dan satu tombol kirim. Aksi server akan melakukan validasi dasar untuk memeriksa apakah email diberikan dan apakah formatnya valid.
Pertama, mari kita definisikan aksi server kita. Jika Anda menggunakan Next.js, Anda dapat menempatkan ini di file yang sama dengan komponen Anda dengan menambahkan direktif 'use server'; di bagian atas fungsi.
// Di actions.js atau di bagian atas file komponen Anda dengan 'use server'
export async function subscribe(previousState, formData) {
const email = formData.get('email');
if (!email) {
return { message: 'Email wajib diisi.' };
}
// Regex sederhana untuk tujuan demonstrasi
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) {
return { message: 'Silakan masukkan alamat email yang valid.' };
}
// Di sini Anda biasanya akan menyimpan email ke database
console.log(`Berlangganan dengan email: ${email}`);
// Mensimulasikan jeda
await new Promise(res => setTimeout(res, 1000));
return { message: 'Terima kasih telah berlangganan!' };
}
Sekarang, mari kita buat komponen klien yang menggunakan aksi ini dengan useFormState.
'use client';
import { useFormState } from 'react-dom';
import { subscribe } from './actions';
const initialState = {
message: null,
};
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
<h3>Berlangganan Buletin Kami</h3>
<div>
<label htmlFor="email">Alamat Email</label>
<input type="email" id="email" name="email" required />
</div>
<button type="submit">Berlangganan</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
Mari kita uraikan apa yang terjadi:
- Kita mengimpor
useFormStatedarireact-dom(perhatikan: bukanreact). - Kita mendefinisikan objek
initialState. Ini memastikan variabelstatekita memiliki bentuk yang konsisten sejak render pertama. - Kita memanggil
useFormState(subscribe, initialState). Ini menghubungkan state komponen kita ke aksi serversubscribe. formActionyang dikembalikan diteruskan ke propactionelemen<form>. Inilah koneksi ajaibnya.- Kita merender pesan dari objek
statekita secara kondisional. Pada render pertama,state.messageadalahnull, jadi tidak ada yang ditampilkan. - Ketika pengguna mengirimkan formulir, React memanggil
formAction. Ini memicu aksi serversubscribekita. FungsisubscribemenerimapreviousState(awalnya,initialStatekita) danformData. - Aksi server menjalankan logikanya dan mengembalikan objek state baru (misalnya,
{ message: 'Email wajib diisi.' }). - React menerima state baru ini dan me-render ulang komponen
SubscriptionForm. Variabelstatesekarang berisi objek baru, dan paragraf kondisional kita menampilkan pesan kesalahan atau keberhasilan.
Ini sangat kuat. Kita telah mengimplementasikan loop validasi klien-server penuh dengan boilerplate manajemen state sisi klien yang minimal.
Meningkatkan UX dengan `useFormStatus`
Formulir kita berfungsi, tetapi pengalaman pengguna bisa lebih baik. Ketika pengguna mengklik "Berlangganan", tombol tetap aktif, dan tidak ada indikasi visual bahwa sesuatu sedang terjadi sampai server merespons. Di sinilah hook useFormStatus berperan.
Hook useFormStatus menyediakan informasi status tentang pengiriman formulir terakhir. Yang terpenting, hook ini harus digunakan dalam komponen yang merupakan anak dari elemen <form>. Ini tidak akan berfungsi jika dipanggil di komponen yang sama yang merender formulir.
Mari kita buat komponen SubmitButton terpisah.
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Mengirim...' : 'Berlangganan'}
</button>
);
}
Sekarang, kita dapat memperbarui SubscriptionForm kita untuk menggunakan komponen baru ini:
// ... impor
import { SubmitButton } from './SubmitButton';
// ... initialState dan kode lainnya
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
{/* ... input formulir ... */}
<SubmitButton /> {/* Gantikan tombol lama */}
{state?.message && <p>{state.message}</p>}
</form>
);
}
Dengan perubahan ini, ketika formulir dikirim, nilai pending dari useFormStatus menjadi true. Komponen SubmitButton kita di-render ulang, menonaktifkan tombol dan mengubah teksnya menjadi "Mengirim...". Setelah aksi server selesai dan useFormState memperbarui state, formulir tidak lagi dalam status pending, dan tombol kembali ke keadaan semula. Ini memberikan umpan balik penting kepada pengguna dan mencegah pengiriman ganda.
Validasi Lanjutan dengan State Error Terstruktur dan Zod
Satu pesan string sudah cukup untuk formulir sederhana, tetapi aplikasi dunia nyata seringkali memerlukan pesan kesalahan validasi per-bidang. Kita dapat dengan mudah mencapai ini dengan mengembalikan objek state yang lebih terstruktur dari aksi server kita.
Mari kita tingkatkan aksi kita untuk mengembalikan objek dengan kunci errors, yang berisi pesan untuk bidang tertentu. Ini juga merupakan kesempatan sempurna untuk memperkenalkan pustaka validasi skema seperti Zod untuk logika validasi yang lebih kuat dan mudah dipelihara.
Langkah 1: Instal Zod
npm install zod
Langkah 2: Perbarui Aksi Server
Kita akan membuat skema Zod untuk mendefinisikan bentuk dan aturan validasi yang diharapkan untuk data formulir kita. Kemudian, kita akan menggunakan schema.safeParse() untuk memvalidasi formData yang masuk.
'use server';
import { z } from 'zod';
// Definisikan skema untuk formulir kita
const contactSchema = z.object({
name: z.string().min(2, { message: 'Nama harus minimal 2 karakter.' }),
email: z.string().email({ message: 'Alamat email tidak valid.' }),
message: z.string().min(10, { message: 'Pesan harus minimal 10 karakter.' }),
});
export async function submitContactForm(previousState, formData) {
const validatedFields = contactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// Jika validasi gagal, kembalikan error
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Validasi gagal. Silakan periksa input Anda.',
};
}
// Jika validasi berhasil, proses data
// Contohnya, kirim email atau simpan ke database
console.log('Sukses!', validatedFields.data);
// ... logika pemrosesan ...
// Kembalikan state sukses
return {
errors: {},
message: 'Terima kasih atas pesan Anda! Kami akan segera menghubungi Anda.',
};
}
Perhatikan bagaimana kita menggunakan validatedFields.error.flatten().fieldErrors. Ini adalah utilitas Zod yang praktis yang mengubah objek error menjadi struktur yang lebih mudah digunakan, seperti: { name: ['Nama harus minimal 2 karakter.'], message: ['Pesan terlalu pendek'] }.
Langkah 3: Perbarui Komponen Klien
Sekarang, kita akan memperbarui komponen formulir kita untuk menangani state error yang terstruktur ini.
'use client';
import { useFormState } from 'react-dom';
import { submitContactForm } from './actions';
import { SubmitButton } from './SubmitButton'; // Asumsikan kita memiliki tombol submit
const initialState = {
message: null,
errors: {},
};
export function ContactForm() {
const [state, formAction] = useFormState(submitContactForm, initialState);
return (
<form action={formAction}>
<h2>Hubungi Kami</h2>
<div>
<label htmlFor="name">Nama</label>
<input type="text" id="name" name="name" />
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" />
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="message">Pesan</label>
<textarea id="message" name="message" />
{state.errors?.message && (
<p className="error">{state.errors.message[0]}</p>
)}
</div>
<SubmitButton />
{state.message && <p className="form-status">{state.message}</p>}
</form>
);
}
Pola ini sangat skalabel dan kuat. Aksi server Anda menjadi satu-satunya sumber kebenaran untuk logika validasi, dan Zod menyediakan cara yang deklaratif dan aman-tipe (type-safe) untuk mendefinisikan aturan-aturan tersebut. Komponen klien hanya menjadi konsumen dari state yang disediakan oleh useFormState, menampilkan error di tempat yang semestinya. Pemisahan tanggung jawab ini membuat kode lebih bersih, lebih mudah diuji, dan lebih aman, karena validasi selalu ditegakkan di server.
`useFormState` vs. Solusi Manajemen Form Lainnya
Dengan adanya alat baru, muncul pertanyaan: "Kapan saya harus menggunakan ini daripada yang sudah saya ketahui?" Mari kita bandingkan useFormState dengan pendekatan umum lainnya.
`useFormState` vs. `useState`
- `useState` sempurna untuk formulir sederhana yang hanya ada di klien atau ketika Anda perlu melakukan interaksi sisi klien yang kompleks dan real-time (seperti validasi langsung saat pengguna mengetik) sebelum pengiriman. Ini memberi Anda kontrol langsung dan terperinci.
- `useFormState` unggul ketika state formulir terutama ditentukan oleh respons server. Ini dirancang untuk siklus permintaan/respons dari pengiriman formulir dan merupakan pilihan utama saat menggunakan Server Actions. Ini menghilangkan kebutuhan untuk mengelola panggilan fetch, state loading, dan penguraian respons secara manual.
`useFormState` vs. Pustaka Pihak Ketiga (React Hook Form, Formik)
Pustaka seperti React Hook Form dan Formik adalah solusi matang dan kaya fitur yang menawarkan serangkaian alat komprehensif untuk manajemen formulir. Mereka menyediakan:
- Validasi sisi klien tingkat lanjut (seringkali dengan integrasi skema untuk Zod, Yup, dll.).
- Manajemen state yang kompleks untuk bidang bersarang (nested fields), array bidang, dan lainnya.
- Optimisasi kinerja (misalnya, mengisolasi render ulang hanya ke input yang berubah).
- Pembantu untuk komponen terkontrol dan integrasi dengan pustaka UI.
Jadi, kapan Anda memilih yang mana?
- Pilih
useFormStateketika:- Anda menggunakan React Server Actions dan menginginkan solusi asli yang terintegrasi.
- Sumber kebenaran validasi utama Anda adalah server.
- Anda menghargai progressive enhancement dan ingin formulir Anda berfungsi tanpa JavaScript.
- Logika formulir Anda relatif sederhana dan berpusat pada siklus pengiriman/respons.
- Pilih pustaka pihak ketiga ketika:
- Anda memerlukan validasi sisi klien yang ekstensif dan kompleks dengan umpan balik langsung (misalnya, validasi saat on blur atau on change).
- Anda memiliki formulir yang sangat dinamis (misalnya, menambah/menghapus bidang, logika kondisional).
- Anda tidak menggunakan kerangka kerja dengan Server Actions dan membangun lapisan komunikasi klien-server Anda sendiri dengan REST atau GraphQL API.
- Anda memerlukan kontrol terperinci atas kinerja dan render ulang dalam formulir yang sangat besar.
Penting juga untuk dicatat bahwa ini tidak saling eksklusif. Anda dapat menggunakan React Hook Form untuk mengelola state dan validasi sisi klien dari formulir Anda, lalu menggunakan handler pengirimannya untuk memanggil Server Action. Namun, untuk banyak kasus penggunaan umum, kombinasi useFormState dan Server Actions memberikan solusi yang lebih sederhana dan lebih elegan.
Praktik Terbaik dan Kesalahan Umum
Untuk mendapatkan hasil maksimal dari useFormState, pertimbangkan praktik terbaik berikut:
- Jaga Agar Aksi Tetap Fokus: Fungsi aksi formulir Anda harus bertanggung jawab untuk satu hal: memproses pengiriman formulir. Ini termasuk validasi, mutasi data (menyimpan ke DB), dan mengembalikan state baru. Hindari efek samping yang tidak terkait dengan hasil formulir.
- Definisikan Bentuk State yang Konsisten: Selalu mulai dengan
initialStateyang terdefinisi dengan baik dan pastikan aksi Anda selalu mengembalikan objek dengan bentuk yang sama, bahkan saat berhasil. Ini mencegah kesalahan runtime di klien saat mencoba mengakses properti sepertistate.errors. - Rangkul Progressive Enhancement: Ingatlah bahwa Server Actions berfungsi tanpa JavaScript sisi klien. Rancang UI Anda untuk menangani kedua skenario dengan baik. Misalnya, pastikan pesan validasi yang di-render di server jelas, karena pengguna tidak akan mendapat manfaat dari status tombol yang dinonaktifkan tanpa JS.
- Pisahkan Urusan UI: Gunakan komponen seperti
SubmitButtonkita untuk merangkum UI yang bergantung pada status. Ini membuat komponen formulir utama Anda lebih bersih dan menghormati aturan bahwauseFormStatusharus digunakan di komponen anak. - Jangan Lupakan Aksesibilitas: Saat menampilkan error, gunakan atribut ARIA seperti
aria-invalidpada bidang input Anda dan kaitkan pesan error dengan input masing-masing menggunakanaria-describedbyuntuk memastikan formulir Anda dapat diakses oleh pengguna pembaca layar.
Kesalahan Umum: Menggunakan useFormStatus di Komponen yang Sama
Kesalahan yang sering terjadi adalah memanggil useFormStatus di komponen yang sama yang merender tag <form>. Ini tidak akan berfungsi karena hook perlu berada di dalam konteks formulir untuk mengakses statusnya. Selalu ekstrak bagian UI Anda yang membutuhkan status (seperti tombol) ke dalam komponen anaknya sendiri.
Kesimpulan
Hook useFormState, bersama dengan Server Actions, merupakan evolusi signifikan dalam cara kita menangani formulir di React. Ini mendorong pengembang ke arah model validasi yang lebih kuat dan berpusat pada server sambil menyederhanakan manajemen state sisi klien. Dengan mengabstraksikan kompleksitas siklus hidup pengiriman, ini memungkinkan kita untuk fokus pada hal yang paling penting: mendefinisikan logika bisnis kita dan membangun pengalaman pengguna yang mulus.
Meskipun mungkin tidak menggantikan pustaka pihak ketiga yang komprehensif untuk setiap kasus penggunaan, useFormState menyediakan fondasi yang kuat, asli, dan ditingkatkan secara progresif untuk sebagian besar formulir dalam aplikasi web modern. Dengan menguasai polanya dan memahami tempatnya di ekosistem React, Anda dapat membangun formulir yang lebih tangguh, mudah dipelihara, dan ramah pengguna dengan lebih sedikit kode dan kejelasan yang lebih besar.